🛠️ 环境准备第 0 步
本次实验需要 同时打开两个 MySQL 会话(窗口 A、窗口 B),分别模拟两个事务。建议直接打开两个 mysql 命令行,或者用 Workbench / DBeaver / Navicat 开两个连接。
第 1 步:建库建表
在任一会话中执行下面的 SQL,建好"银行账户表"。
DROP DATABASE IF EXISTS bankaccount;
CREATE DATABASE bankaccount DEFAULT CHARACTER SET utf8mb4;
USE bankaccount;
CREATE TABLE account (
id VARCHAR(20) PRIMARY KEY,
R DECIMAL(10,2) NOT NULL -- 余额
);
INSERT INTO account VALUES
('account1', 500),
('account2', 900),
('account3', 900);
SELECT * FROM account;
第 2 步:两个会话都连上 bankaccount
之后所有实验都假设你 同时 有:
- 窗口 A 模拟事务 T1
- 窗口 B 模拟事务 T2
在两个窗口里都先执行:
USE bankaccount;
每做完一个实验,务必把数据重置回初始值,否则会影响下一个实验:
UPDATE account SET R = 500 WHERE id = 'account1';
UPDATE account SET R = 900 WHERE id = 'account2';
UPDATE account SET R = 900 WHERE id = 'account3';
📝 第一部分 · 概念巩固不动手 · 10 分钟
先做几道概念题,把讲义里的核心概念再过一遍。看时序表判断异常类型,是必考点。
关于事务,下列说法 错误 的是?
📖 查看答案
✅ 答案:C 错误。
事务的"隔离性"决定了 中间状态不应该 被其他事务看到,否则就出现"脏读"了。
A 描述事务定义;B 是原子性;D 是持久性 —— 都对。
下面 4 个场景对应 ACID 中的哪个性质?(在心里配对)
① 转账时断电了,重启后系统自动撤销了已扣款,账户回到原值。
② 即使服务器重启,已成功提交的转账仍然在。
③ 100 个用户同时操作,A 账户被扣的钱总等于 B 账户被加的钱。
④ 我的事务在跑时,感觉像是只有我一个人在用数据库。
📖 查看答案
① → A 原子性(中途失败要撤销)
② → D 持久性(已提交就不丢)
③ → C 一致性(数据满足业务规则)
④ → I 隔离性(事务感觉不到其他事务)
观察下面的时序表,判断属于哪种数据不一致性?
| 时间 | 事务 T1 | R 的值 | 事务 T2 |
|---|---|---|---|
| t1 | SELECT R = 200 | 200 | — |
| t2 | — | 200 | SELECT R = 200 |
| t3 | UPDATE R = 200-50 = 150 | 150 | — |
| t4 | — | 170 ❌ | UPDATE R = 200-30 = 170 |
📖 查看答案与分析
✅ 答案:A · 丢失更新
关键看 t4:T2 基于"过期的 200"算出 170 写回 —— 把 T1 在 t3 的修改 覆盖 了。
正确结果应该是 200 - 50 - 30 = 120,实际变成 170 —— T1 扣的 50 元消失了。这就是丢失更新。
观察下面的时序表,判断属于哪种数据不一致性?
| 时间 | 事务 T1 | R 的值 | 事务 T2 |
|---|---|---|---|
| t1 | SELECT R = 1000 | 1000 | — |
| t2 | — | 1000 | UPDATE R = 800 |
| t3 | — | 800 | COMMIT |
| t4 | SELECT R = 800 ≠ 1000 ❌ | 800 | — |
📖 查看答案与分析
✅ 答案:C · 不可重复读
关键判断:
① t3 已经 COMMIT 了 —— 排除"脏读"(脏读必须是读到未提交)
② T1 同一事务读 同一行 两次,值不一样(1000 → 800)—— 这是不可重复读
③ 不是行数变了 —— 不是幻象读
把下面这张表的空格填上(用 √ 表示能避免,✗ 表示不能):
| 隔离级别 | 脏读 | 不可重复读 | 幻象读 |
|---|---|---|---|
| READ UNCOMMITTED | ? | ? | ? |
| READ COMMITTED | ? | ? | ? |
| REPEATABLE READ | ? | ? | ? |
| SERIALIZABLE | ? | ? | ? |
📖 查看答案
| 隔离级别 | 脏读 | 不可重复读 | 幻象读 |
|---|---|---|---|
| READ UNCOMMITTED | ✗ | ✗ | ✗ |
| READ COMMITTED | √ | ✗ | ✗ |
| REPEATABLE READ ⭐ | √ | √ | ✗ |
| SERIALIZABLE | √ | √ | √ |
💡 记忆:每升一级,多挡一种问题,从低到高是脏读 → 不可重复读 → 幻象读。
🔬 第二部分 · 上机实验动手做 · 30 分钟
本部分是核心! 你要 亲眼看到 脏读、不可重复读、锁阻塞这些现象。每个实验前先重置数据,避免互相干扰。
把 T1 设为 READ UNCOMMITTED(最低级别),看 T1 怎么读到 T2 未提交的数据。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM account; -- 第一次查询,记下 account1 的余额
START TRANSACTION;
UPDATE account SET R = R + 100 WHERE id = 'account1';
-- 注意:这里千万不要 COMMIT!
SELECT * FROM account; -- 第二次查询
在两个窗口都执行 ROLLBACK; 撤销操作,然后用前面的"重置数据"SQL 把数据恢复成 500/900/900。
READ UNCOMMITTED 下,T1 读到了 T2 还没提交的数据。这正是讲义里讲的"脏读"。
现在把 T1 升级到 READ COMMITTED,看脏读怎么被挡住。但要小心:升一级以后 还有不可重复读问题。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM account; -- 第一次查询:account1 = 500
START TRANSACTION;
UPDATE account SET R = R + 100 WHERE id = 'account1';
-- 不要 COMMIT!
SELECT * FROM account; -- 第二次查询
COMMIT;
SELECT * FROM account; -- 第三次查询
READ COMMITTED 挡住了脏读 ✅,但挡不住不可重复读 ❌
实验完毕:在两个窗口执行 COMMIT; 或 ROLLBACK;,然后重置数据。
把 T1 升级到 REPEATABLE READ(MySQL 默认级别),看不可重复读怎么被挡住。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM account; -- 第一次查询:account1 = 500
START TRANSACTION;
UPDATE account SET R = R + 100 WHERE id = 'account1';
COMMIT; -- 这次直接提交
SELECT * FROM account; -- 第二次查询
COMMIT;
SELECT * FROM account; -- 现在能看到 B 的修改了:600
REPEATABLE READ 下,同一事务内多次读取同一行得到的结果一致,与外部事务无关。这就是"可重复读"。
实验完毕后记得 重置数据。
动手感受 X 锁(写锁)怎么阻塞别人。
LOCK TABLES account WRITE; -- WRITE = X 锁
SELECT * FROM account;
SELECT * FROM account; -- 试图读取
UNLOCK TABLES;
UNLOCK,窗口 B 立刻得到结果! X 锁释放,B 的查询可以执行了。
X 锁(写锁)是 独占锁 —— 别人连读都不行。这正是讲义里"S+S 才能并存,X 排斥一切"的体现。
看 S 锁(读锁)怎么允许多人共读,但谁都不能写。
LOCK TABLES account READ; -- READ = S 锁
SELECT * FROM account; -- 能读 ✅
LOCK TABLES account READ; -- 也加 S 锁
SELECT * FROM account; -- 也能读
UPDATE account SET R = 100 WHERE id = 'account1';
错误信息类似:
Table 'account' was locked with a READ lock and can't be updated
两个窗口都执行 UNLOCK TABLES; 释放锁。
验证了讲义里的两个核心规则:
① S + S 兼容(多人共读 ✅)
② 持 S 锁时,连自己都不能写(S 锁是只读锁)
🎯 第三部分 · 综合应用10 分钟
最后做几道综合题,把本章知识连起来。
某个银行系统对 账户余额 的查询和修改要求很高,不能容忍脏读和不可重复读,但能接受偶尔的幻象读(业务上可以容忍)。应该选哪个隔离级别?
📖 查看答案
✅ 答案:C · REPEATABLE READ
分析:
• 不能脏读 → A 排除
• 不能不可重复读 → B 排除(B 挡不住不可重复读)
• 能容忍幻象读 → 不需要 D(D 太严格性能差)
• 所以选 C,这也是 MySQL 的默认级别。
事务 T1 已经对表 R 加了 S 锁。下面 4 个操作,哪个会被阻塞?(多选)
- ① 事务 T2 对 R 加 S 锁
- ② 事务 T2 对 R 加 X 锁
- ③ 事务 T2 修改 R 的某行(UPDATE)
- ④ 事务 T1 自己 SELECT R
📖 查看答案
✅ 答案:② 和 ③ 会被阻塞
原因:
① S 锁 + S 锁 → 兼容(共读),不阻塞 ✅
② S 锁 + X 锁 → 不兼容,阻塞 ❌
③ UPDATE 需要 X 锁 → 不兼容,阻塞 ❌
④ T1 自己持 S 锁,能读,不会阻塞自己 ✅
口诀:只有 S+S 兼容,其他都阻塞。
下面是两个事务的执行顺序,请回答:会不会发生死锁?为什么?如何避免?
| 时间 | 事务 T1 | 事务 T2 |
|---|---|---|
| t1 | 锁定"学生表" | — |
| t2 | — | 锁定"成绩表" |
| t3 | 请求"成绩表" | — |
| t4 | — | 请求"学生表" |
📖 查看答案与分析
✅ 会发生死锁。
原因:T1 拿着学生表锁,等成绩表;T2 拿着成绩表锁,等学生表 —— 互相等对方释放,谁也动不了。
怎么避免:用 顺序加锁法 —— 规定 所有事务都按相同顺序 锁表,比如规定都先锁"学生表"再锁"成绩表"。这样 T2 也是先锁学生表,会被 T1 阻塞等待,但不会出现循环等待,所以不会死锁。
修改后的顺序示意:
- T1:先锁学生表 → 锁成绩表 → 处理 → 解锁
- T2:也先锁学生表 → 等待 T1 释放 → 拿到学生表锁 → 再锁成绩表 → 处理 → 解锁
📝 实验小结
通过本次实验你应该已经:
- ✅ 能识别 4 种数据不一致性 的时序表(必考点)
- ✅ 亲眼看到脏读、不可重复读怎么发生 —— 不再是抽象的概念
- ✅ 会用
SET TRANSACTION ISOLATION LEVEL设置隔离级别 - ✅ 会用
LOCK TABLES READ/WRITE加锁解锁 - ✅ 理解了"S+S 兼容、X 排斥一切"的实际表现
下次考试本章一定考的内容:
- 看时序表 判断属于哪种数据不一致性(必考!多练)
- 四种隔离级别和三种问题的 对照表(必背)
- 事务的 ACID 四大性质 各是什么意思(简答题)
- X 锁、S 锁的兼容性(哪种组合能并存)